
package de.justphil.tcg.tcgserver.game.mechanics.event;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import de.justphil.tcg.tcgserver.commons.exceptions.TCGException;
import de.justphil.tcg.tcgserver.game.mechanics.game.Game;
import de.justphil.tcg.tcgserver.game.mechanics.game.GameDataSerializable;
import de.justphil.tcg.tcgserver.game.mechanics.util.tools.IterationHelper;

import org.slf4j.Logger;
import static de.justphil.tcg.tcgserver.commons.logging.TCGLogging.*;

/**
 * 
 * @author Martin
 * 
 */
public class EventManager implements EventObservable, Scheduler, GameDataSerializable {
	
	private static final Logger					log			= gameLogger(EventManager.class);
	private double								time		= 0.0;
	private boolean								stop		= false;
	private boolean								firstRun	= true;
	private boolean								running		= false;
	private Map<Double, EventList>				eventTimeMap;
	private Map<Integer, List<OnEventListener>>	eventListenerMap;
	
	public EventManager() {
	
		eventListenerMap = new HashMap<Integer, List<OnEventListener>>();
		eventTimeMap = new HashMap<Double, EventList>();
	}
	
	public boolean isRunning() {
	
		return running;
	}
	
	@Override
	public double getTotalTime() {
	
		return time;
	}
	
	@Override
	public double getTimeToEvent(Event event) {
	
		Collection<Double> times = eventTimeMap.keySet();
		for (double time : times) {
			EventList events = eventTimeMap.get(time);
			if (events != null && events.size() > 0) {
				for (Event scheduledEvent : events) {
					if (scheduledEvent.equals(event)) {
						return time - this.time;
					}
				}
			}
		}
		return -1.0;
	}
	
	public void run() {
	
		if (running) {
			log.error("Trying to start the EventManager twice!!!");
			assert false;
			return;
		}
		running = true;
		log.info("The EventManager has started.", new Object[] {});
		if (firstRun) {
			try {
				scheduleFirst(new Event(Event.EVENT_START, null), this.time);
			}
			catch (TCGException e) {
				assert false : e.getMessage();
			}
			firstRun = false;
		}
		do {
			EventList events = eventTimeMap.get(time);
			assert events != null;
			do {
				signalEvent(events.removeFirst());
			}
			while (events.size() > 0 && !stop);
			/* Removing the time from the event list after all events have been worked through. */
			if (events.size() == 0) {
				eventTimeMap.remove(time);
			}
		}
		while (!stop && jumpInTime());
		if (!stop) {
			running = false;
			log.info("The End: All Events have been signaled.");
		}
		else {
			stop = false;
			running = false;
			log.info("The EventManager has stopped.", new Object[] {});
		}
	}
	
	private boolean jumpInTime() {
	
		Set<Double> times = eventTimeMap.keySet();
		if (times.size() == 0) {
			log.debug("No Events scheduled to jump to.");
			return false;
		}
		else {
			double minTime = Double.MAX_VALUE;
			for (double time : times) {
				if (time < minTime) {
					minTime = time;
				}
			}
			time = minTime;
			EventList events = eventTimeMap.get(time);
			if (events == null || events.size() == 0) {
				log.warn("Jumping to a time where no Events are Scheduled!", new Object[] {});
				assert false;
				return false;
			}
			log.info("-----Jumping to {}-------------------", time);
			return true;
		}
	}
	
	/**
	 * Sets the stop variable to true. The EventManager will stop. If started again, it will resume
	 * with the scheduled Events.
	 */
	public void stop() {
	
		if (stop == false && running == true) {
			log.info("Stopping the EventManager.", new Object[] {});
			this.stop = true;
		}
	}
	
	@Override
	public void addOnEventListener(int eventType, OnEventListener listener) {
	
		List<OnEventListener> listeners = this.eventListenerMap.get(eventType);
		if (listeners == null) {
			listeners = new LinkedList<OnEventListener>();
			this.eventListenerMap.put(eventType, listeners);
		}
		if (!listeners.contains(listener)) {
			listeners.add(listener);
			log.debug("Added listener of class {} for Event type {}.", new Object[] {
					listener.getClass().getSimpleName(), Event.getTypeAsString(eventType) });
		}
		else {
			log.warn("Trying to add a listener {} for Event type {} twice!", new Object[] {
					listener, Event.getTypeAsString(eventType) });
		}
	}
	
	@Override
	public boolean removeOnEventListener(int eventType, OnEventListener listener) {
	
		List<OnEventListener> listeners = this.eventListenerMap.get(eventType);
		if (listeners != null) {
			boolean removed = !listeners.remove(listener);
			if (removed) {
				log.warn(
						"Could not remove a listener of class {} and Event type {}!",
						new Object[] {
								listener.getClass().getSimpleName(),
								Event.getTypeAsString(eventType) });
				return false;
			}
			else {
				log.debug("Removed a listener of class {} and Event type {}!", new Object[] {
						listener.getClass().getSimpleName(), Event.getTypeAsString(eventType) });
				return true;
			}
		}
		else {
			return false;
		}
	}
	
	@Override
	public void scheduleFirst(Event event, double relativeTime) throws TCGException {
	
		schedule(event, relativeTime, true);
	}
	
	@Override
	public void scheduleLast(Event event, double relativeTime) throws TCGException {
	
		schedule(event, relativeTime, false);
	}
	
	@Override
	public void scheduleNow(Event event) throws TCGException {
	
		if (getTimeToEvent(event) != -1) {
			throw new TCGException("An Event could not be scheduled!");
		}
		signalEvent(event);
	}
	
	@Override
	public boolean unschedule(final Event event) {
	
		Collection<Double> times = new LinkedList<Double>(eventTimeMap.keySet());
		for (double time : times) {
			EventList events = eventTimeMap.get(time);
			if (events != null) {
				if (events.remove(event)) {
					log.debug("Unscheduled an Event ({}) at time {}!", new Object[] {
							event, time });
					/* A corresponding Event of type EVENT_SCHEDULED must also be removed. */
					{
						Event scheduleEvent = new Event(Event.EVENT_EVENT_SCHEDULED, event);
						if (eventTimeMap.get(this.time).remove(scheduleEvent)) {
							log.debug(
									"   Unscheduled the corresponding scheduling-Event ({}) at time {}!",
									new Object[] {
											event, time });
						}
					}
					if (events.size() == 0) {
						log.debug("No further Events are scheduled at time {}!", new Object[] {
							time });
						/* if the event time is the recent time the EventList must not be removed
						 * from the eventTimeMap! */
						if (time > this.time) {
							eventTimeMap.remove(time);
						}
					}
					return true;
				}
			}
		}
		log.warn("Could not unschedule an Event ({}) because it has never been scheduled!",
				new Object[] {
					event });
		assert false;
		return false;
	}
	
	@Override
	public void writeData(Game game, DataOutput out) throws IOException {
	
		if (running) {
			log.error("The serialization of a running EventManager will fail!", new Object[] {});
			assert false;
		}
		/* Primitives */
		{
			out.writeDouble(time);
			out.writeBoolean(stop);
			out.writeBoolean(firstRun);
			// out.writeBoolean(running);
		}
		/* eventTimeMap */
		{
			out.writeInt(eventTimeMap.size());
			for (Double d : eventTimeMap.keySet()) {
				out.writeDouble(d.doubleValue());
				EventList el = eventTimeMap.get(d);
				out.writeInt(el.size());
				for (Event e : el) {
					e.writeData(game, out);
				}
			}
		}
	}
	
	@Override
	public void readData(Game game, DataInput in) throws IOException {
	
		/* Primitives */
		{
			time = in.readDouble();
			stop = in.readBoolean();
			firstRun = in.readBoolean();
			// running = in.readBoolean();
		}
		/* eventTimeMap */
		{
			eventTimeMap = new HashMap<Double, EventList>();
			int eventTimeMapSize = in.readInt();
			Event e = null;
			EventList el = null;
			int elSize = 0;
			double eventTime = 0;
			for (int i = 0; i < eventTimeMapSize; i++) {
				eventTime = in.readDouble();
				elSize = in.readInt();
				el = new EventList();
				for (int j = 0; j < elSize; j++) {
					e = new Event();
					e.readData(game, in);
					el.add(e);
				}
				eventTimeMap.put(eventTime, el);
			}
		}
	}
	
	private void schedule(Event event, double relativeTime, boolean first) throws TCGException {
	
		double scheduleTime = time + relativeTime;
		if (relativeTime < 0) {
			log.warn(
					"Could not schedule an Event ({}) at time {}! The Scheduling time must be equal "
							+ "to or grater than {}!", new Object[] {
							event, scheduleTime, time });
			assert false;
			throw new TCGException("An Event could not be scheduled!");
		}
		else if (relativeTime == Double.POSITIVE_INFINITY) {
			log.warn(
					"Could not schedule an Event ({}) at time {}! The Scheduling time must not be infinety!",
					new Object[] {
							event, scheduleTime });
			assert false;
			throw new TCGException(
					"An Event could not be scheduled! The Scheduling time must not be infinety!");
		}
		else {
			EventList events = eventTimeMap.get(scheduleTime);
			if (events == null) {
				events = new EventList();
				eventTimeMap.put(scheduleTime, events);
			}
			if (getTimeToEvent(event) != -1) {
				log.debug(
						"Could not schedule an Event ({}) at time {}! An equal Event has already been scheduled at {}!",
						new Object[] {
								event, scheduleTime, getTimeToEvent(event) });
				throw new TCGException(
						"An Event could not be scheduled! An equal Event has already been scheduled!");
			}
			if (first) {
				events.addFirst(event);
				// if (event.getType() != Event.EVENT_EVENT_SCHEDULED) {
				log.debug("Scheduled an Event ({}) at time {} at the beginning!", new Object[] {
						event, scheduleTime });
				// }
			}
			else {
				events.addLast(event);
				// if (event.getType() != Event.EVENT_EVENT_SCHEDULED) {
				log.debug("Scheduled an Event ({}) at time {} at the end!", new Object[] {
						event, scheduleTime });
				// }
			}
			if (event.getType() != Event.EVENT_EVENT_SCHEDULED) {
				scheduleFirst(new Event(Event.EVENT_EVENT_SCHEDULED, event), 0.0);
			}
		}
	}
	
	/**
	 * Executes the onEvent() method of all listeners which are registered for this event type. The
	 * execution will be sequential!
	 */
	private void signalEvent(final Event event) {
	
		// if (event.getType() != Event.EVENT_EVENT_SCHEDULED) {
		log.debug("Event ({}).", event);
		// }
		List<OnEventListener> listeners = this.eventListenerMap.get(event.getType());
		if (listeners == null) {
			return;
		}
		new IterationHelper<OnEventListener>(listeners) {
			
			@Override
			protected void execute(OnEventListener listElement) {
			
				listElement.onEvent(event);
			}
		}.safeExecuteForAll();
	}
}
